<!-- 拖拽排序组件 -->
<!-- XXX 暂时是专门给添加问题类别使用的，后续考虑抽离成单独的包，可以给其他项目使用 -->
<template>
  <div class="drag-and-sort">
    <!-- 各个类别 -->
    <div class="item existing-item" v-for="(item, index) in props.modelValue" :key="item"
      :class="item === activatedItem ? 'active' : ''" @click="itemClick(item)" :data-item="item"
      @touchstart="itemTouchStart" @touchmove="itemTouchMove" @touchend="itemTouchEnd"
      :style="isLongPress ? allItemStyles[index] : {}">
      {{ item }}
    </div>
    <!-- 「新建类别」按钮 -->
    <div class="new-item item" @click="addClick">
      <image class="green-plus-icon" src="@/static/icons/common/green-plus.svg" />
      新建类别
    </div>
  </div>

  <!-- 「新建类别」弹窗 -->
  <uni-popup ref="addQuestionTypePopup" mask-background-color="rgba(0,0,0,.6)" type="dialog">
    <div class="popup-container">
      <!-- 中间的弹窗 -->
      <div class="popup">
        <div class="title">新建类别</div>
        <input class="input" :focus="newQuestionTypeNameInputFocus" type="text" placeholder="test1"
          :adjust-position="false" placeholder-style="color: #9f9f9f;" v-model="newQuestionCategoryName" />
        <div class="btn confirm-btn" @click="onAddQuestionTypeConfirmBtnClick">确定</div>
      </div>
      <!-- 取消按钮 -->
      <img class="cancel-btn" src="@/static/icons/common/close-circle.svg" @click="toggleAddItemPopup(false)">
    </div>
  </uni-popup>
</template>

<script setup lang="ts">
import { showToast } from '@/utils/tools';
import type { CSSProperties } from 'vue';
import { nextTick } from 'vue';
import { onMounted } from 'vue';
import { getCurrentInstance } from 'vue';
import { ref } from 'vue';

/** 当前组件实例 */
const instance = getCurrentInstance()

const props = defineProps<{
  /** 绑定值 */
  modelValue: Array<string>
  /** 当前激活的值 */
  activatedItem: string
}>()

const emit = defineEmits<{
  (e: 'update:modelValue', value: Array<string>): void,
  (e: 'update', value: Array<string>): void,
  /** 添加新的item */
  (e: 'add', value: string): void,
  /** 修改激活的item */
  (e: 'change', value: string): void
  /** 修改item的排序 */
  (e: 'sort', value: Array<string>): void
}>()



/** 更新分类列表数据 */
const updateValue = (value: Array<string>) => {
  try {
    emit('update:modelValue', value)
    emit('update', value)
  } catch (e) {
    console.error('更新数据失败', e);
  }
}

//#region 激活item相关

/** 当前激活的item的索引 */
const activatedItem = ref<string>('')

/** item点击事件 */
const itemClick = (item: string) => {
  console.log('itemClick', item)
  // 如果是长按，则不触发点击事件
  if (isLongPress.value) {
    isLongPress.value = false
    return
  }
  activatedItem.value = item
  emit('change', item)
}

// 页面挂载后，将参数对应的item激活
onMounted(() => {
  activatedItem.value = props.activatedItem
})

//#endregion

// #region 新建类别相关

/** 添加按钮点击事件 */
const addClick = () => {
  toggleAddItemPopup(true)
}

/** 新建类别的名称 */
const newQuestionCategoryName = ref('')

/** 类别名称输入框是否获取焦点 */
const newQuestionTypeNameInputFocus = ref(false)

/**
 * 切换弹窗开关状态
 * @param {boolean} [isShow] 是否显示弹窗
 */
const toggleAddItemPopup = (isShow: boolean) => {
  const popup: any = instance?.refs?.addQuestionTypePopup
  if (isShow) {
    popup?.open()
    // 如果太快获取焦点，会导致占位文字出现闪动
    setTimeout(() => {
      newQuestionTypeNameInputFocus.value = true
    }, 300);
  } else {
    popup?.close()
    newQuestionTypeNameInputFocus.value = false
  }
}

/** 「新建类别」弹窗中的确定按钮点击事件 */
const onAddQuestionTypeConfirmBtnClick = () => {
  console.log('props', props.modelValue)
  // 判断新的类别名称是否为空、是否已存在、是否超过6个字符
  if (!newQuestionCategoryName.value) {
    showToast('类别不能为空')
    return
  }
  if (props.modelValue.includes(newQuestionCategoryName.value)) {
    showToast('类别已存在')
    return
  }
  if (newQuestionCategoryName.value.length > 6) {
    showToast('类别不能超过6个字')
    return
  }

  updateValue([...props.modelValue, newQuestionCategoryName.value])
  activatedItem.value = props.modelValue[props.modelValue.length - 1]
  emit('add', newQuestionCategoryName.value)

  newQuestionCategoryName.value = ''
  toggleAddItemPopup(false)

  // 更新容器和item的坐标及尺寸
  nextTick(() => {
    getContainerRectangle()
    getAllItemRectangles()
  })
}

// #endregion

//#region 拖拽相关

/** 根据item返回其对应的index（因为列表渲染不会更新index） */
const getIndexByItem = (item: string) => {
  return props.modelValue.findIndex(i => i === item)
}

/** 容器坐标及尺寸 */
const containerRectangle = {
  x: 0,
  y: 0,
  width: 0,
  height: 0
}

/** 获取并记录容器的坐标及尺寸 */
const getContainerRectangle = () => {
  const selectorQuery = uni.createSelectorQuery().in(instance)
  selectorQuery
    .select('.drag-and-sort')
    .boundingClientRect((r) => {
      const rect = r as UniApp.NodeInfo
      containerRectangle.x = rect.left || 0
      containerRectangle.y = rect.top || 0
      containerRectangle.width = rect.width || 0
      containerRectangle.height = rect.height || 0
    })
    .exec()
  console.log('containerRectangle', containerRectangle)
}

/** 所有已有item的坐标、长宽信息 */
let itemRectangles: Array<Rectangle> = []

/** 所有已有item的样式 */
const allItemStyles = ref<Array<CSSProperties>>([])

/** 获取并记录所有item的坐标、长宽信息，并且生成其style对象 */
const getAllItemRectangles = () => {
  const selectorQuery = uni.createSelectorQuery().in(instance)
  selectorQuery
    .selectAll('.existing-item')
    .boundingClientRect((rect) => {
      console.log('rect', rect)
      console.log('test')
      // NOTE rect的顺序是一开始的顺序，而不是当前的顺序
      // 根据rect中各项的dataset.item属性来判断其对应的index，并且根据index来排序
      itemRectangles = (rect as UniApp.NodeInfo[]).sort((a, b) => {
        const aIndex = getIndexByItem(a.dataset.item)
        const bIndex = getIndexByItem(b.dataset.item)
        return aIndex - bIndex
      }).map(nodeRect => {
        return {
          x: nodeRect.left || 0,
          y: nodeRect.top || 0,
          width: nodeRect.width || 0,
          height: nodeRect.height || 0
        }
      });

      console.log('itemRectangles', itemRectangles)
      allItemStyles.value = itemRectangles.map(() => {
        return {
          translate: '0 0',
          transitionProperty: 'translate',
          transitionDuration: '0.3s',
          transitionTimingFunction: 'ease-in-out'
        }
      })
    })
    .exec()
}

onMounted(() => {
  getAllItemRectangles()
  getContainerRectangle()
})

/** 开始触摸的坐标 */
const touchStartCoordinate = { x: 0, y: 0 }

/** 被拖拽的元素的index */
let draggedItemIndex = -1

/** 被插入的位置的index */
let insertedItemIndex = -1

/** 是否为长按 */
let isLongPress = ref(false)

/** 长按定时器 */
let longPressTimeout: number

/**
 * 判断item是否到达了容器边界
 * @param {Rectangle} itemRect item的坐标及尺寸
 * @param {Rectangle} containerRect 容器的坐标及尺寸
 * @param limit 边界限制值，默认为0
 * @returns {boolean} 是否到达了容器边界
 */
const isItemReachContainerBoundary = (itemRect: Rectangle, containerRect: Rectangle, limit: number = 0) => {
  return itemRect.x < containerRect.x - limit
    || itemRect.y < containerRect.y - limit
    || itemRect.x + itemRect.width > containerRect.x + containerRect.width + limit
    || itemRect.y + itemRect.height > containerRect.y + containerRect.height + limit
}

/** 触摸开始事件 */
const itemTouchStart = (e: TouchEvent) => {
  // 1秒后判定为长按
  longPressTimeout = setTimeout(() => {
    console.log('long press', e)
    // @ts-ignore ts提示没有dataset，但实际上是有的
    const item = e.currentTarget?.dataset?.item
    draggedItemIndex = props.modelValue.findIndex(i => i === item)
    // 放大被拖拽的元素
    allItemStyles.value[draggedItemIndex] = {
      scale: 1.1,
      transitionProperty: 'scale',
      transitionDuration: '0.3s',
      zIndex: 999,
    }

    const { clientX, clientY } = e.touches[0]
    touchStartCoordinate.x = clientX
    touchStartCoordinate.y = clientY
    isLongPress.value = true
  }, 300);
}


/** 触摸移动事件 */
const itemTouchMove = (e: TouchEvent) => {
  // 如果不是长按，不处理
  clearTimeout(longPressTimeout)
  if (!isLongPress.value) return


  const { clientX, clientY } = e.touches[0]
  const offsetX = clientX - touchStartCoordinate.x
  const offsetY = clientY - touchStartCoordinate.y

  const itemRealRect = {
    x: itemRectangles[draggedItemIndex].x + offsetX,
    y: itemRectangles[draggedItemIndex].y + offsetY,
    width: itemRectangles[draggedItemIndex].width,
    height: itemRectangles[draggedItemIndex].height
  }
  // 如果item到达了容器边界，不处理
  if (isItemReachContainerBoundary(itemRealRect, containerRectangle, 50)) return

  allItemStyles.value[draggedItemIndex] = {
    scale: 1.1,
    translate: `${offsetX}px ${offsetY}px`,
    transitionProperty: 'scale, translate',
    transitionDuration: '0s',
    zIndex: 999,
  }
  /** 被拖拽元素中心的X坐标 */
  const draggedItemX = itemRectangles[draggedItemIndex].x + itemRectangles[draggedItemIndex].width / 2 + offsetX
  /** 被拖拽元素中心的Y坐标 */
  const draggedItemY = itemRectangles[draggedItemIndex].y + itemRectangles[draggedItemIndex].height / 2 + offsetY

  insertedItemIndex = getInsertedItemIndex(draggedItemIndex, { x: draggedItemX, y: draggedItemY }, itemRectangles)
  // 如果没有进入可触发插入的区域，则复原位置
  if (insertedItemIndex === -1) {
    resetPosition('except')
    return
  }

  // 如果进入了可触发插入的区域，则将当前元素插入到可触发插入的元素的位置
  setTranslate(draggedItemIndex, insertedItemIndex, false)
}


/** 触摸结束事件 */
const itemTouchEnd = (e: TouchEvent) => {
  clearTimeout(longPressTimeout)
  if (!isLongPress.value) return

  // 如果没有进入可触发插入的区域，则复原位置
  if (insertedItemIndex === -1) {
    resetPosition('all')
    isLongPress.value = false
    return
  }

  // 如果进入了可触发插入的区域，则将当前元素插入到可触发插入的元素的位置
  setTranslate(draggedItemIndex, insertedItemIndex, true)

  // 交换数据
  const newModelValue = [...props.modelValue]
  const draggedItem = newModelValue.splice(draggedItemIndex, 1)[0]
  newModelValue.splice(insertedItemIndex, 0, draggedItem)
  console.log('newModelValue', newModelValue)
  setTimeout(() => {
    updateValue(newModelValue)
    emit('sort', newModelValue)
    isLongPress.value = false
    getAllItemRectangles()
    getContainerRectangle()
  }, 100);
}

/**
 * 获取可触发插入的元素的index
 * @param index 被拖拽的元素的index
 * @param touchPosition 当前手指的位置，{x: number, y: number}
 * @param rectangles 所有item的坐标、长宽信息
 * @returns number 如果进入了可触发插入的区域，则返回可交换的元素的index，否则返回-1
 */
const getInsertedItemIndex = (index: number, touchPosition: { x: number, y: number }, rectangles: Array<Rectangle>) => {
  for (let i = 0; i < rectangles.length; i++) {
    // 跳过自身
    if (i === index) continue
    const { x: itemX, y: itemY, width: itemWidth, height: itemHeight } = rectangles[i]
    // 如果触摸坐标在item的区域内，则判定为可触发插入
    if (touchPosition.x >= itemX && touchPosition.x <= itemX + itemWidth && touchPosition.y >= itemY && touchPosition.y <= itemY + itemHeight) {
      return i
    }
  }
  return -1
}

/**
 * 根据被拖拽元素的index和可触发插入的元素的index，计算出并修改所有其他元素的translate
 * @param draggedIndex 被拖拽元素的index
 * @param pluggableIndex 可触发插入的元素的index
 * @param isSetDraggedIndex 是否设置被拖拽元素的translate
 * @returns void
 */
const setTranslate = (draggedIndex: number, pluggableIndex: number, isSetDraggedIndex: boolean) => {
  allItemStyles.value = allItemStyles.value.map((style, index) => {
    // 还原回原位置的情况：
    // 如果在被拖拽元素的后面，且同时在被插入位置的后面
    // 如果在被拖拽元素的前面，且同时在被插入位置的前面
    if ((index > draggedIndex && index > pluggableIndex) || (index < draggedIndex && index < pluggableIndex)) {
      return {
        translate: '0 0',
        transitionProperty: 'translate',
        transitionDuration: '0.3s',
        transitionTimingFunction: 'ease-in-out'
      }
    }

    // 如果是被拖拽的元素，且不设置被拖拽元素的translate，则不需要移动
    if (index === draggedIndex && !isSetDraggedIndex) return style

    // 如果是被拖拽的元素，且设置被拖拽元素的translate，则需要移动
    if (index === draggedIndex && isSetDraggedIndex) {
      const { x: itemX, y: itemY } = itemRectangles[pluggableIndex]
      const { x: draggedItemX, y: draggedItemY } = itemRectangles[draggedIndex]
      const offsetX = itemX - draggedItemX
      const offsetY = itemY - draggedItemY
      return {
        ...style,
        translate: `${offsetX}px ${offsetY}px`,
        transitionDuration: '0.1s',
        zIndex: 999,
      }
    }

    // 如果在被拖拽元素的后面，且同时在被插入位置的前面，则向前移动
    if (index > draggedIndex && index <= pluggableIndex) {
      const { x: itemX, y: itemY } = itemRectangles[index]
      const { x: beforeItemX, y: beforeItemY } = itemRectangles[index - 1]
      const offsetX = beforeItemX - itemX
      const offsetY = beforeItemY - itemY
      return {
        ...style,
        translate: `${offsetX}px ${offsetY}px`,
        zIndex: 998,
      }
    }

    // 如果在被拖拽元素的前面，且同时在被插入位置的后面，则向后移动
    if (index < draggedIndex && index >= pluggableIndex) {
      const { x: itemX, y: itemY } = itemRectangles[index]
      const { x: afterItemX, y: afterItemY } = itemRectangles[index + 1]
      const offsetX = afterItemX - itemX
      const offsetY = afterItemY - itemY
      return {
        ...style,
        translate: `${offsetX}px ${offsetY}px`,
        zIndex: 998,
      }
    }

    return style
  })
}

/**
 * 还原各元素位置（除被拖拽元素）
 * @param type 类型，all：还原所有元素，except：还原除被拖拽元素以外的元素
 */
const resetPosition = (type: 'all' | 'except') => {
  allItemStyles.value = allItemStyles.value.map((style, index) => {
    if (type === 'except' && index === draggedItemIndex) return style
    return {
      ...style,
      translate: '0 0',
      transitionDuration: '0.3s',
      zIndex: 0,
    }
  })
}


//#endregion
</script>

<style scoped lang="scss">
.drag-and-sort {
  width: 100%;

  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px;
  justify-items: flex-start;


  .item {
    min-width: calc(20vw + 20px);
    height: 86.54rpx;
    background: #f5f5f5;
    border-radius: 75rpx;

    // box-sizing: content-box;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 0 10px;
    // 文字不换行
    white-space: nowrap;

    font-size: 28.85rpx;
    color: rgba(0, 0, 0, 0.40);
  }

  // 「新建类别」按钮
  .new-item {
    color: #17ce92;

    .green-plus-icon {
      width: 19.23rpx;
      height: 19.23rpx;
    }
  }

  // 选中状态
  .active {
    background: #17ce92;
    color: #fff;
  }
}

// 「新建类别」弹窗
.popup-container {
  width: 76.9231vw;
  height: 53.0769vw;

  display: grid;
  grid-template-rows: 1fr auto;
  grid-template-columns: 1fr;
  grid-row-gap: 39px;
  justify-items: center;

  // 弹窗主体
  .popup {
    width: 76.9231vw;
    height: 49.7436vw;
    background: #ffffff;
    border-radius: 4px;

    box-sizing: border-box;
    padding: 16px;
    display: grid;
    grid-template:
      'title' 21px
      '.' 24px
      'input' 1fr
      '40px'
      'btn' 36px;
    justify-items: center;

    // 标题
    .title {
      grid-area: title;

      font-size: 3.8462vw;
      color: #333;
    }

    // 输入框
    .input {
      grid-area: input;
      align-items: center;

      width: 68.7179vw;
      min-height: 10.5128vw;
      border: 1px solid #17ce92;
      border-radius: 4px;

      box-sizing: border-box;
      padding-left: 16px;

      font-size: 3.8462vw;
    }

    // 「确定」按钮
    .btn {
      grid-area: btn;

      width: 268px;
      height: 36px;
      background: #17ce92;
      border-radius: 4px;

      // 文字居中
      display: flex;
      justify-content: center;
      align-items: center;

      font-size: 3.5897vw;
      color: #fff;
    }
  }

  // 取消按钮
  .cancel-btn {
    width: 8.7179vw;
    height: 8.7179vw;
  }
}
</style>
